package lmscript.postscript;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

import lmscript.postscript.io.ASCII85InputStream;
import lmscript.postscript.io.ASCIIHexInputStream;
import lmscript.postscript.syntax.PSComment;
import lmscript.postscript.syntax.PSInteger;
import lmscript.postscript.syntax.PSName;
import lmscript.postscript.syntax.PSNumber;
import lmscript.postscript.syntax.PSObject;
import lmscript.postscript.syntax.PSRadix;
import lmscript.postscript.syntax.PSReal;
import lmscript.postscript.syntax.PSString;

public class Scanner {
	public static final char LF = '\r';
	public static final char CR = '\n';
	public static final char TAB = '\t';
	public static final char SP = ' ';
	public static final char FF = 0x0c;
	public static final char NULL = 0x00;
	public static final char BACKSPACE = 0x05;

	private PushbackInputStream in;
	private int lineNo;
	private StringBuffer bytesBuf = new StringBuffer();
	private NamedObjectLookup lookup;

	public Scanner(InputStream is) {
		this.in = new PushbackInputStream(is);
	}

	public PSObject next() throws IOException {
		PSObject obj = nextObject();
		
		return obj;
	}

	private PSObject nextObject() throws IOException {
		int c = in.read();

		switch (c) {
		case -1:
			return null;
		case '/':
			return readPSName(c);
		case '+':
		case '-':
		case '.':
		case '0':
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			return readNumber(c);
		case '(':
		case '<': {
			int next = this.in.read();
			if (next == '<') {
				return new PSName("<<");
			} else {
				this.in.unread(next);
				return readString(c);
			}
		}
		case '%': {
			int next = this.in.read();
			if (next == '%') {
				this.readDSC();
			} else {
				this.in.unread(next);
				return readComment();
			}
		}
		default:
			return readPSName(c);
		}
	}

	private PSName readPSName(int last) throws IOException {
		this.clearBuf();
		if (last != '/') {
			this.appendToBuf(last);
		}
		int ch = -1;
		boolean stop = false;
		while (!stop) {
			ch = this.in.read();
			switch (ch) {
			case -1:
			case LF:
			case CR:
			case TAB:
			case SP:
			case FF:
			case NULL:
			case BACKSPACE: {
				stop = true;
				break;
			}
			case '%':
			case '(':
			case '/':
			case '<':
			case '>':
			case '[':
			case ']':
			case '{':
			case '}': {
				stop = true;
				this.in.unread(ch);
				break;
			}
			default: {
				this.appendToBuf(ch);
				break;
			}
			}
		}
		return new PSName(this.getBufString());
	}

	private PSObject readDSC() throws IOException {
		return null;
	}

	private PSNumber readNumber(int first) throws IOException {
		clearBuf();
		appendToBuf(first);
		int next = this.in.read();

		boolean stop = false;
		while (!stop) {
			switch (next) {
			case ' ':
				stop = true;
				break;
			case LF: {
				next = this.in.read();
				if (next == CR) {
					this.lineNo++;
				}
				stop = true;
				break;
			}
			case CR: {
				this.lineNo++;
				stop = true;
				break;
			}
			default: {
				appendToBuf(next);
				break;
			}
			}
			next = this.in.read();
		}

		return parseNumber(getBufString());
	}

	private PSString readString(int left) throws IOException {
		if (left == '(') {
			return readLiteralText();
		}
		if (left == '<') {
			int next = this.in.read();
			if (next == '~') {
				return readASCII85Text();
			} else {
				this.in.unread(next);
				return readASCIIHexText();
			}
		}
		return null;
	}

	private PSString readLiteralText() throws IOException {
		this.clearBuf();
		int c = this.in.read();
		boolean stop = false;
		while (stop) {
			switch (c) {
			case ')': {
				stop = true;
				break;
			}
			case LF: {
				int next = this.in.read();
				if (next == CR) {
					this.lineNo++;
				}
				break;
			}
			case CR: {
				this.lineNo++;
				break;
			}
			default: {
				this.appendToBuf(c);
				break;
			}
			}
			if (!stop) {
				c = this.in.read();
			}
		}
		return new PSString(this.getBufString());
	}

	private PSString readASCII85Text() throws IOException {
		ASCII85InputStream asc85InputStream = new ASCII85InputStream(this.in);
		this.clearBuf();
		int ch = asc85InputStream.read();
		while (ch != -1) {
			this.bytesBuf.append(ch);
			ch = asc85InputStream.read();
		}
		this.lineNo += asc85InputStream.lineCount();
		return new PSString(this.getBufString());
	}

	private PSString readASCIIHexText() throws IOException {
		ASCIIHexInputStream hexInputStream = new ASCIIHexInputStream(this.in);
		this.clearBuf();
		int ch = hexInputStream.read();
		while (ch != -1) {
			this.bytesBuf.append(ch);
			ch = hexInputStream.read();
		}
		this.lineNo += hexInputStream.lineCount();
		return new PSString(this.getBufString());
	}

	private PSComment readComment() {
		return null;
	}

	private PSNumber parseNumber(String text) {
		if (text.length() <= 0) {
			return null;
		}
		if (text.indexOf('#') > 0) {
			return new PSRadix(text);
		} else if (text.indexOf('.') >= 0 || (text.indexOf('E') > 0)) {
			return new PSReal(text);
		}
		return new PSInteger(text);
	}

	private void clearBuf() {
		this.bytesBuf.delete(0, this.bytesBuf.length() - 1);
	}

	private void appendToBuf(int c) {
		this.bytesBuf.append(c);
	}

	private String getBufString() {
		return this.bytesBuf.toString();
	}
}
